// Copyright (c) 2024 CHANGLEI. All rights reserved.

import 'package:charts_grasp/src/chart/axis/axis.dart';
import 'package:charts_grasp/src/chart/chart_series/series.dart';
import 'package:charts_grasp/src/chart/common/cartesian_state_properties.dart';
import 'package:charts_grasp/src/chart/common/interactive_tooltip.dart';
import 'package:charts_grasp/src/chart/common/trackball_marker_settings.dart';
import 'package:charts_grasp/src/chart/user_interaction/trackball.dart';
import 'package:charts_grasp/src/chart/user_interaction/trackball_marker_setting_renderer.dart';
import 'package:charts_grasp/src/chart/user_interaction/trackball_painter.dart';
import 'package:charts_grasp/src/chart/utils/enum.dart';
import 'package:charts_grasp/src/common/rendering_details.dart';
import 'package:charts_grasp/src/common/utils/enum.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class TrackballTemplate extends StatefulWidget {
  const TrackballTemplate({required Key key, required this.stateProperties, required this.trackballBehavior})
      : super(key: key);

  final CartesianStateProperties stateProperties;

  final TrackballBehavior trackballBehavior;

  @override
  State<StatefulWidget> createState() {
    return TrackballTemplateState();
  }
}

class TrackballTemplateState extends State<TrackballTemplate> {
  bool _isRender = false;


  List<ChartPointInfo>? chartPointInfo;

  List<Path>? markerShapes;

  late TrackballGroupingModeInfo groupingModeInfo;
  late Widget _template;

  late double duration;

  bool? alwaysShow;

  bool _isRangeSeries = false;
  bool _isBoxSeries = false;

  @override
  void initState() {
    super.initState();
  }

  @override
  void didUpdateWidget(TrackballTemplate oldWidget) {
    super.didUpdateWidget(oldWidget);
  }

  @override
  Widget build(BuildContext context) {
    Widget trackballWidget;
    final List<Widget> trackballWidgets = <Widget>[];
    String seriesType;
    if (_isRender &&
        widget.stateProperties.animationCompleted &&
        chartPointInfo != null &&
        chartPointInfo!.isNotEmpty) {
      for (int index = 0; index < chartPointInfo!.length; index++) {
        seriesType = chartPointInfo![index].seriesRendererDetails!.seriesType;
        _isRangeSeries = seriesType.contains('range') || seriesType.contains('hilo') || seriesType == 'candle';
        _isBoxSeries = seriesType == 'boxandwhisker';
        if (widget.trackballBehavior.tooltipDisplayMode == TrackballDisplayMode.groupAllPoints) {
          _template =
              widget.trackballBehavior.builder!(context, TrackballDetails(null, null, null, null, groupingModeInfo));
          trackballWidget = _TrackballRenderObject(
              template: _template,
              stateProperties: widget.stateProperties,
              xPos: chartPointInfo![index].xPosition!,
              yPos: (_isRangeSeries
                  ? chartPointInfo![index].highYPosition
                  : _isBoxSeries
                      ? chartPointInfo![index].maxYPosition
                      : chartPointInfo![index].yPosition)!,
              trackballBehavior: widget.trackballBehavior,
              child: _template,);

          trackballWidgets.add(trackballWidget);

          break;
        } else if (widget.trackballBehavior.tooltipDisplayMode != TrackballDisplayMode.none) {
          _template = widget.trackballBehavior.builder!(
              context,
              TrackballDetails(
                  chartPointInfo![index].seriesRendererDetails!.dataPoints[chartPointInfo![index].dataPointIndex!],
                  chartPointInfo![index].seriesRendererDetails!.series,
                  chartPointInfo![index].dataPointIndex,
                  chartPointInfo![index].seriesIndex,),);

          trackballWidget = _TrackballRenderObject(
              template: _template,
              stateProperties: widget.stateProperties,
              xPos: chartPointInfo![index].xPosition!,
              yPos: (_isRangeSeries
                  ? chartPointInfo![index].highYPosition
                  : _isBoxSeries
                      ? chartPointInfo![index].maxYPosition
                      : chartPointInfo![index].yPosition)!,
              trackballBehavior: widget.trackballBehavior,
              chartPointInfo: chartPointInfo,
              index: index,
              child: _template,);

          trackballWidgets.add(trackballWidget);
        }
      }
      return Stack(children: <Widget>[
        CustomPaint(
            painter: TracklinePainter(widget.trackballBehavior, widget.stateProperties, chartPointInfo, markerShapes),),
        Stack(children: trackballWidgets),
      ],);
    } else {
      trackballWidget = Container();
      return trackballWidget;
    }
  }

  void refresh() {
    if (mounted) {
      setState(() {
        _isRender = true;
      });
    }
  }

  void hideTrackballTemplate() {
    if (mounted && alwaysShow != null && (!alwaysShow! || !widget.stateProperties.isTouchUp)) {
      setState(() {
        _isRender = false;
      });
    }
  }
}

@immutable
class _TrackballRenderObject extends SingleChildRenderObjectWidget {
  const _TrackballRenderObject({
    required Widget super.child,
    required this.template,
    required this.stateProperties,
    required this.xPos,
    required this.yPos,
    required this.trackballBehavior,
    this.chartPointInfo,
    this.index,
  });

  final Widget template;
  final int? index;
  final CartesianStateProperties stateProperties;
  final List<ChartPointInfo>? chartPointInfo;
  final double xPos;
  final double yPos;
  final TrackballBehavior trackballBehavior;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return TrackballTemplateRenderBox(template, stateProperties, xPos, yPos, chartPointInfo, index);
  }

  @override
  void updateRenderObject(BuildContext context, covariant TrackballTemplateRenderBox renderBox) {
    renderBox.template = template;
    renderBox.index = index;
    renderBox.xPos = xPos;
    renderBox.yPos = yPos;
    renderBox.chartPointInfo = chartPointInfo;
  }
}

class TrackballTemplateRenderBox extends RenderShiftedBox {
  TrackballTemplateRenderBox(this._template, this.stateProperties, this.xPos, this.yPos,
      [this.chartPointInfo, this.index, RenderBox? child,])
      : super(child);

  Widget _template;

  final CartesianStateProperties stateProperties;

  double xPos;
  double yPos;

  List<ChartPointInfo>? chartPointInfo;

  int? index;

  late double pointerLength;
  late double pointerWidth;

  Rect? trackballTemplateRect;

  late Rect boundaryRect;

  num padding = 10;

  late TrackballBehavior trackballBehavior;

  bool isGroupAllPoints = false;

  bool isNearestPoint = false;

  Widget get template => _template;

  bool isRight = false;

  bool isBottom = false;

  bool isTemplateInBounds = true;

  TooltipPositions? tooltipPosition;

  late BoxParentData childParentData;

  TrackballRenderingDetails get trackballRenderingDetails =>
      TrackballHelper.getRenderingDetails(stateProperties.trackballBehaviorRenderer);

  set template(Widget value) {
    if (_template != value) {
      _template = value;
      markNeedsLayout();
    }
  }

  @override
  void performLayout() {
    trackballBehavior = stateProperties.chart.trackballBehavior;
    isGroupAllPoints = trackballBehavior.tooltipDisplayMode == TrackballDisplayMode.groupAllPoints;
    isNearestPoint = trackballBehavior.tooltipDisplayMode == TrackballDisplayMode.nearestPoint;
    final List<num> tooltipTop = <num>[];
    final List<num> tooltipBottom = <num>[];
    final List<ClosestPoints> visiblePoints = trackballRenderingDetails.visiblePoints;
    final List<ChartAxisRenderer> xAxesInfo = trackballRenderingDetails.xAxesInfo;
    final List<ChartAxisRenderer> yAxesInfo = trackballRenderingDetails.yAxesInfo;
    boundaryRect = stateProperties.chartAxis.axisClipRect;
    final num totalWidth = boundaryRect.left + boundaryRect.width;
    tooltipPosition = trackballRenderingDetails.tooltipPosition;
    final bool isTrackballMarkerEnabled = trackballBehavior.markerSettings != null;
    final BoxConstraints constraints = this.constraints;
    pointerLength = trackballBehavior.tooltipSettings.arrowLength;
    pointerWidth = trackballBehavior.tooltipSettings.arrowWidth;
    double left;
    double top;
    Offset? offset;
    if (child != null) {
      child!.layout(constraints, parentUsesSize: true);
      size = constraints.constrain(Size(child!.size.width, child!.size.height));
      if (child!.parentData is BoxParentData) {
        childParentData = child!.parentData as BoxParentData;

        if (isGroupAllPoints) {
          if (trackballBehavior.tooltipAlignment == ChartAlignment.center) {
            yPos = boundaryRect.center.dy - size.height / 2;
          } else if (trackballBehavior.tooltipAlignment == ChartAlignment.near) {
            yPos = boundaryRect.top;
          } else {
            yPos = boundaryRect.bottom;
          }

          if (yPos + size.height > boundaryRect.bottom && trackballBehavior.tooltipAlignment == ChartAlignment.far) {
            yPos = boundaryRect.bottom - size.height;
          }
        }
        if (chartPointInfo != null) {
          for (int index = 0; index < chartPointInfo!.length; index++) {
            tooltipTop.add(stateProperties.requireInvertedAxis
                ? visiblePoints[index].closestPointX - (size.width / 2)
                : visiblePoints[index].closestPointY - size.height / 2,);
            tooltipBottom.add(stateProperties.requireInvertedAxis
                ? visiblePoints[index].closestPointX + (size.width / 2)
                : visiblePoints[index].closestPointY + size.height / 2,);
            xAxesInfo.add(chartPointInfo![index].seriesRendererDetails!.xAxisDetails!.axisRenderer);
            yAxesInfo.add(chartPointInfo![index].seriesRendererDetails!.yAxisDetails!.axisRenderer);
          }
        }
        if (tooltipTop.isNotEmpty) {
          tooltipPosition = trackballRenderingDetails.smartTooltipPositions(
              tooltipTop, tooltipBottom, xAxesInfo, yAxesInfo, chartPointInfo!, stateProperties.requireInvertedAxis,);
        }

        double yPlotOffset = yPos;
        if (yPos + size.height > boundaryRect.bottom) {
          yPlotOffset = yPos - size.height;
          if (yPlotOffset < boundaryRect.top) {
            yPlotOffset = boundaryRect.top;
          }
        }

        if (!isGroupAllPoints) {
          left = (stateProperties.requireInvertedAxis
                  ? tooltipPosition!.tooltipTop[index!]
                  : xPos + padding + (isTrackballMarkerEnabled ? trackballBehavior.markerSettings!.width / 2 : 0))
              .toDouble();
          top = (stateProperties.requireInvertedAxis
                  ? yPos + pointerLength + (isTrackballMarkerEnabled ? trackballBehavior.markerSettings!.width / 2 : 0)
                  : tooltipPosition!.tooltipTop[index!])
              .toDouble();

          if (isNearestPoint) {
            left = stateProperties.requireInvertedAxis
                ? xPos + size.width / 2
                : xPos + padding + (isTrackballMarkerEnabled ? trackballBehavior.markerSettings!.width / 2 : 0);
            top = stateProperties.requireInvertedAxis
                ? yPos + padding + (isTrackballMarkerEnabled ? trackballBehavior.markerSettings!.width / 2 : 0)
                : yPos - size.height / 2;
          }

          if (!stateProperties.requireInvertedAxis) {
            if (left + size.width > totalWidth) {
              isRight = true;
              left = xPos -
                  size.width -
                  pointerLength -
                  (isTrackballMarkerEnabled ? (trackballBehavior.markerSettings!.width / 2) : 0);
            } else {
              isRight = false;
            }
          } else {
            if (top + size.height > boundaryRect.bottom) {
              isBottom = true;
              top = yPos -
                  pointerLength -
                  size.height -
                  (isTrackballMarkerEnabled ? (trackballBehavior.markerSettings!.height) : 0);
            } else {
              isBottom = false;
            }
          }
          trackballTemplateRect = Rect.fromLTWH(left, top, size.width, size.height);
          double xPlotOffset = visiblePoints.first.closestPointX - trackballTemplateRect!.width / 2;
          final double rightTemplateEnd = xPlotOffset + trackballTemplateRect!.width;
          final double leftTemplateEnd = xPlotOffset;

          if (_isTemplateWithinBounds(boundaryRect, trackballTemplateRect!, offset)) {
            isTemplateInBounds = true;
            childParentData.offset = Offset(left, top);
          } else if (boundaryRect.width > trackballTemplateRect!.width &&
              boundaryRect.height > trackballTemplateRect!.height) {
            isTemplateInBounds = true;
            if (rightTemplateEnd > boundaryRect.right) {
              xPlotOffset = xPlotOffset - (rightTemplateEnd - boundaryRect.right);
              if (xPlotOffset < boundaryRect.left) {
                xPlotOffset = xPlotOffset + (boundaryRect.left - xPlotOffset);
                if (xPlotOffset + trackballTemplateRect!.width > boundaryRect.right) {
                  xPlotOffset = xPlotOffset - (totalWidth + trackballTemplateRect!.width - boundaryRect.right);
                }
                if (xPlotOffset < boundaryRect.left || xPlotOffset > boundaryRect.right) {
                  isTemplateInBounds = false;
                }
              }
            } else if (leftTemplateEnd < boundaryRect.left) {
              xPlotOffset = xPlotOffset + (boundaryRect.left - leftTemplateEnd);
              if (xPlotOffset + trackballTemplateRect!.width > boundaryRect.right) {
                xPlotOffset = xPlotOffset - (totalWidth + trackballTemplateRect!.width - boundaryRect.right);
                if (xPlotOffset < boundaryRect.left) {
                  xPlotOffset = xPlotOffset + (boundaryRect.left - xPlotOffset);
                }
                if (xPlotOffset < boundaryRect.left ||
                    xPlotOffset + trackballTemplateRect!.width > boundaryRect.right) {
                  isTemplateInBounds = false;
                }
              }
            }
            childParentData.offset = Offset(xPlotOffset, yPlotOffset);
          } else {
            child!.layout(constraints.copyWith(maxWidth: 0), parentUsesSize: true);
            isTemplateInBounds = false;
          }
        } else {
          if (xPos + size.width > totalWidth) {
            xPos = xPos -
                size.width -
                2 * padding -
                (isTrackballMarkerEnabled ? trackballBehavior.markerSettings!.width / 2 : 0);
          }

          trackballTemplateRect = Rect.fromLTWH(xPos, yPos, size.width, size.height);
          double xPlotOffset = visiblePoints.first.closestPointX - trackballTemplateRect!.width / 2;
          final double rightTemplateEnd = xPlotOffset + trackballTemplateRect!.width;
          final double leftTemplateEnd = xPlotOffset;

          if (_isTemplateWithinBounds(boundaryRect, trackballTemplateRect!, offset) &&
              (boundaryRect.right > trackballTemplateRect!.right && boundaryRect.left < trackballTemplateRect!.left)) {
            isTemplateInBounds = true;
            childParentData.offset = Offset(
                xPos +
                    (trackballTemplateRect!.right + padding > boundaryRect.right
                        ? trackballTemplateRect!.right + padding - boundaryRect.right
                        : padding) +
                    (isTrackballMarkerEnabled ? trackballBehavior.markerSettings!.width / 2 : 0),
                yPlotOffset,);
          } else if (boundaryRect.width > trackballTemplateRect!.width &&
              boundaryRect.height > trackballTemplateRect!.height) {
            isTemplateInBounds = true;
            if (rightTemplateEnd > boundaryRect.right) {
              xPlotOffset = xPlotOffset - (rightTemplateEnd - boundaryRect.right);
              if (xPlotOffset < boundaryRect.left) {
                xPlotOffset = xPlotOffset + (boundaryRect.left - xPlotOffset);
                if (xPlotOffset + trackballTemplateRect!.width > boundaryRect.right) {
                  xPlotOffset = xPlotOffset - (totalWidth + trackballTemplateRect!.width - boundaryRect.right);
                }
                if (xPlotOffset < boundaryRect.left || xPlotOffset > boundaryRect.right) {
                  isTemplateInBounds = false;
                }
              }
            } else if (leftTemplateEnd < boundaryRect.left) {
              xPlotOffset = xPlotOffset + (boundaryRect.left - leftTemplateEnd);
              if (xPlotOffset + trackballTemplateRect!.width > boundaryRect.right) {
                xPlotOffset = xPlotOffset - (xPlotOffset + trackballTemplateRect!.width - boundaryRect.right);
                if (xPlotOffset < boundaryRect.left) {
                  xPlotOffset = xPlotOffset + (boundaryRect.left - xPlotOffset);
                }
                if (xPlotOffset < boundaryRect.left || xPlotOffset > boundaryRect.right) {
                  isTemplateInBounds = false;
                }
              }
            }
            childParentData.offset = Offset(xPlotOffset, yPlotOffset);
          } else {
            child!.layout(constraints.copyWith(maxWidth: 0), parentUsesSize: true);
            isTemplateInBounds = false;
          }
        }
      }
    } else {
      size = Size.zero;
    }
    if (!isGroupAllPoints && index == chartPointInfo!.length - 1) {
      tooltipTop.clear();
      tooltipBottom.clear();
      yAxesInfo.clear();
      xAxesInfo.clear();
    }
  }

  bool _isTemplateWithinBounds(Rect bounds, Rect templateRect, Offset? offset) {
    final Rect rect = Rect.fromLTWH(
        padding + templateRect.left, (3 * padding) + templateRect.top, templateRect.width, templateRect.height,);
    final Rect axisBounds =
        Rect.fromLTWH(padding + bounds.left, (3 * padding) + bounds.top, bounds.width, bounds.height);
    return rect.left >= axisBounds.left &&
        rect.left + rect.width <= axisBounds.left + axisBounds.width &&
        rect.top >= axisBounds.top &&
        rect.bottom <= axisBounds.top + axisBounds.height;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    final bool isTemplateWithInBoundsInTransposedChart =
        _isTemplateWithinBounds(boundaryRect, trackballTemplateRect!, offset);
    if ((!stateProperties.requireInvertedAxis && isTemplateInBounds) ||
        (stateProperties.requireInvertedAxis && isTemplateWithInBoundsInTransposedChart)) {
      super.paint(context, offset);
    }

    final RenderingDetails renderingDetails = stateProperties.renderingDetails;
    if (!isGroupAllPoints) {
      final Rect templateRect = Rect.fromLTWH(offset.dx + trackballTemplateRect!.left,
          offset.dy + trackballTemplateRect!.top, trackballTemplateRect!.width, trackballTemplateRect!.height,);
      final Paint fillPaint = Paint()
        ..color = trackballBehavior.tooltipSettings.color ??
            (chartPointInfo![index!].seriesRendererDetails!.series.color ??
                renderingDetails.chartTheme.crosshairBackgroundColor)
        ..isAntiAlias = false
        ..style = PaintingStyle.fill;
      final Paint strokePaint = Paint()
        ..color = trackballBehavior.tooltipSettings.borderColor ??
            (chartPointInfo![index!].seriesRendererDetails!.series.color ??
                renderingDetails.chartTheme.crosshairBackgroundColor)
        ..strokeWidth = trackballBehavior.tooltipSettings.borderWidth
        ..strokeCap = StrokeCap.butt
        ..isAntiAlias = false
        ..style = PaintingStyle.stroke;
      final Path path = Path();
      if (trackballTemplateRect!.left > boundaryRect.left && trackballTemplateRect!.right < boundaryRect.right) {
        if (!stateProperties.requireInvertedAxis) {
          if (!isRight) {
            path.moveTo(templateRect.left, templateRect.top + templateRect.height / 2 - pointerWidth);
            path.lineTo(templateRect.left, templateRect.bottom - templateRect.height / 2 + pointerWidth);
            path.lineTo(templateRect.left - pointerLength, yPos + offset.dy);
            path.lineTo(templateRect.left, templateRect.top + templateRect.height / 2 - pointerWidth);
          } else {
            path.moveTo(templateRect.right, templateRect.top + templateRect.height / 2 - pointerWidth);
            path.lineTo(templateRect.right, templateRect.bottom - templateRect.height / 2 + pointerWidth);
            path.lineTo(templateRect.right + pointerLength, yPos + offset.dy);
            path.lineTo(templateRect.right, templateRect.top + templateRect.height / 2 - pointerWidth);
          }
        } else if (isTemplateInBounds && isTemplateWithInBoundsInTransposedChart) {
          if (!isBottom) {
            path.moveTo(templateRect.left + templateRect.width / 2 + pointerWidth, templateRect.top);
            path.lineTo(templateRect.right - templateRect.width / 2 - pointerWidth, templateRect.top);
            path.lineTo(xPos + offset.dx, yPos + offset.dy);
          } else {
            path.moveTo(templateRect.left + templateRect.width / 2 + pointerWidth, templateRect.bottom);
            path.lineTo(templateRect.right - templateRect.width / 2 - pointerWidth, templateRect.bottom);
            path.lineTo(xPos + offset.dx, yPos + offset.dy);
          }
        }

        if (isTemplateInBounds) {
          context.canvas.drawPath(path, fillPaint);
          context.canvas.drawPath(path, strokePaint);
        }
      }
    }
  }
}

class ClosestPoints {
  const ClosestPoints({required this.closestPointX, required this.closestPointY});

  final double closestPointX;

  final double closestPointY;
}

class TooltipPositions {
  const TooltipPositions(this.tooltipTop, this.tooltipBottom);

  final List<num> tooltipTop;

  final List<num> tooltipBottom;
}

class TrackballElement {
  TrackballElement(this.label, this.seriesRenderer);

  final String label;

  final CartesianSeriesRenderer? seriesRenderer;

  bool needRender = true;
}
